package com.adityayadav.mini.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.StringTokenizer;

import com.adityayadav.parallelism.core.ITask;


public class WebSocketWorkerTask implements ITask, Cloneable {

	public static final String ACCEPT = "Accept";
    public static final String CONNECTION = "Connection";
    public static final String CONTENT_LENGTH = "Content-Length";
    public static final String CONTENT_TYPE= "Content-Type";
    public static final String HOST = "Host";
    public static final String ORIGIN = "Origin";
    public static final String UPGRADE = "Upgrade";
    public static final String WEBSOCKET_LOCATION = "WebSocket-Location";
    public static final String WEBSOCKET_ORIGIN = "WebSocket-Origin";
    public static final String WEBSOCKET_PROTOCOL = "WebSocket-Protocol";
    public static final String WEB_SOCKET_PROTOCOL_HANDSHAKE = "101 Web Socket Protocol Handshake";
    public static final String UPGRADEVALUE = "Upgrade";
    public static final String SEC_WEBSOCKET_KEY1 = "Sec-WebSocket-Key1";
    public static final String SEC_WEBSOCKET_KEY2 = "Sec-WebSocket-Key2";
    public static final String SEC_WEBSOCKET_LOCATION = "Sec-WebSocket-Location";
    public static final String SEC_WEBSOCKET_ORIGIN = "Sec-WebSocket-Origin";
    public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
    
    public static final String WEBSOCKET = "WebSocket";
    public static final String GET = "GET";
    public static final String FORBIDDEN = "403 Forbidden";
    public static final String OK = "200 OK";
    public static final String HTTP_1_1 = "HTTP/1.1";
    
	protected String webSocketPath = "/websocket";
	protected SelectionKey selectionKey;
	protected ByteBuffer readBuffer = ByteBuffer.allocate(16000);
	protected MiniWebSocketServer server;
	protected String requestMethod = null;
	protected String requestURL = null;
	protected HashMap<String,String> requestHeaders = new HashMap<String,String>();
	protected HTTPRequestParser requestParser = new HTTPRequestParser();
	
	public static final byte WS_PACKET_START_BYTE = (byte)0x00;
	public static final byte WS_PACKET_END_BYTE = (byte) 0xff;
	
	
	public static final int START_STATE = 0;
	public static final int PARSED_GET_STATE = 1;
	public static final int PARSED_HEADERS = 2;
	
	protected int state = START_STATE;
	
	public WebSocketWorkerTask(){}
	
	public WebSocketWorkerTask(String webSocketPath){
		if (webSocketPath != null) this.webSocketPath = webSocketPath;
	}
	
	public String getWebSocketPath() {
		return webSocketPath;
	}

	public void setWebSocketPath(String webSocketPath) {
		this.webSocketPath = webSocketPath;
	}

	public MiniWebSocketServer getServer() {
		return server;
	}

	public void setServer(MiniWebSocketServer server) {
		this.server = server;
	}

	public SelectionKey getSelectionKey() {
		return selectionKey;
	}

	public void setSelectionKey(SelectionKey selectionKey) {
		this.selectionKey = selectionKey;
	}

	@Override
	public void commit() throws Exception {

	}

	@Override
	public void initialize() throws Exception {

	}

	@Override
	public void rollback() throws Exception {

	}

	@Override
	public void run() throws Exception {
		SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

		// Clear out our read buffer so it's ready for new data
		this.readBuffer.clear();

		// Attempt to read off the channel
		int numRead;
		try {
			numRead = socketChannel.read(this.readBuffer);
		} catch (IOException e) {
			// The remote forcibly closed the connection, cancel
			// the selection key and close the channel.
			selectionKey.cancel();
			server.closeSocketChannel(socketChannel);			
			return;
		}

		if (numRead == -1) {
			// Remote entity shut the socket down cleanly. Do the
			// same from our end and cancel the channel.
			server.closeSocketChannel(socketChannel);
			selectionKey.cancel();
			return;
		}

		// this is where you process the message or spawn a ITask
		if (numRead != 0) {
			byte[] message = new byte[numRead];
		    System.arraycopy(readBuffer.array(), 0, message, 0, numRead);
			requestParser.addToBuffer(message);
			
			byte[] line = null;
			while ((line = requestParser.getALine())!= null){
				
				switch(state){
				case START_STATE:
					processGETCommand(line);
					break;
				case PARSED_GET_STATE:
					processHeader(line);
					break;
				case PARSED_HEADERS:
					handleWebSocketFrames(line);
					break;
				}
			}
		}
	}

	private void processGETCommand(byte[] line) throws Exception{
		StringTokenizer st = new StringTokenizer(new String(line)," ",false);
		requestMethod = st.nextToken();
		requestURL = st.nextToken();
		state = PARSED_GET_STATE;
	}
	
	private void processHeader(byte[] line)throws Exception{
		if (line.length ==0){
			state = PARSED_HEADERS;
			handleHttpRequest();
			requestParser.setSeparator(new byte[]{WS_PACKET_END_BYTE});
		}else {
			String lineStr = new String(line);
			int loc = lineStr.indexOf(':');
			String header = lineStr.substring(0,loc);
			String value = lineStr.substring(loc+1).trim();
			requestHeaders.put(header,value);
		}
	}
	
	private void handleHttpRequest() throws Exception {
        // Allow only GET methods.
    	HTTPResponseCreator rc = new HTTPResponseCreator();
    	rc.setHttpVersion(HTTP_1_1);
        if (!requestMethod.equals( GET)) {
        	rc.setResponseStatus(FORBIDDEN);
        	rc.addHeader(CONNECTION, "close");
        	//rc.addHeader(CONTENT_LENGTH, 0+"");
            sendHttpResponse(rc, true);
            return;
        }

        // Send the demo page.
        if (requestURL.equals("/")) {
        	rc.setResponseStatus(OK);

        	byte[] content = WebSocketServerIndexPage.getContent(getWebSocketLocation());
            rc.setContent(content);

            rc.addHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
            rc.addHeader(CONTENT_LENGTH, content.length+"");
            rc.addHeader(CONNECTION, "close");
            
            sendHttpResponse(rc, true);
            return;
        }

        if (requestURL.equals("/clientaccesspolicy.xml")) {
        	rc.setResponseStatus(OK);

        	byte[] content = WebSocketServerClientAccessPolicy.getContent(getWebSocketLocation());
            rc.setContent(content);

            rc.addHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
            rc.addHeader(CONTENT_LENGTH, content.length+"");
            rc.addHeader(CONNECTION, "close");
            
            sendHttpResponse(rc, true);
            return;
        }
        
        if (requestURL.equals("/crossdomain.xml")) {
        	rc.setResponseStatus(OK);

        	byte[] content = WebSocketServerCrossDomain.getContent(getWebSocketLocation());
            rc.setContent(content);

            rc.addHeader(CONTENT_TYPE, "text/html; charset=UTF-8");
            rc.addHeader(CONTENT_LENGTH, content.length+"");
            rc.addHeader(CONNECTION, "close");
            
            sendHttpResponse(rc,true);
            return;
        }
        
        
        // Serve the WebSocket handshake request.
        if (requestURL.equals(webSocketPath) &&
            UPGRADEVALUE.equalsIgnoreCase(requestHeaders.get(CONNECTION)) &&
            WEBSOCKET.equalsIgnoreCase(requestHeaders.get(UPGRADE))) {

            // Create the WebSocket handshake response.
            rc.setResponseStatus(WEB_SOCKET_PROTOCOL_HANDSHAKE);
            rc.addHeader(UPGRADE, WEBSOCKET);
            rc.addHeader(CONNECTION, UPGRADEVALUE);

            
            // Fill in the headers and contents depending on handshake method.
            if (requestHeaders.get(SEC_WEBSOCKET_KEY1)!=null &&
                requestHeaders.get(SEC_WEBSOCKET_KEY2)!=null) {
                // New handshake method with a challenge:
                rc.addHeader(SEC_WEBSOCKET_ORIGIN, requestHeaders.get(ORIGIN));
                rc.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketLocation());
                String protocol = requestHeaders.get(SEC_WEBSOCKET_PROTOCOL);
                if (protocol != null) {
                    rc.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
                }

                // Calculate the answer of the challenge.
                String key1 = requestHeaders.get(SEC_WEBSOCKET_KEY1);
                String key2 = requestHeaders.get(SEC_WEBSOCKET_KEY2);
                int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
                int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
                byte[] clong = requestParser.getNBytes(8, new byte[0]);
                ByteBuffer input = ByteBuffer.allocate(16);
                input.putInt(a);
                input.putInt(b);
                input.put(clong);
                //sendHttpResponse(rc, false);
                byte[] h = rc.getMessage().getBytes();
                byte[] sig = MessageDigest.getInstance("MD5").digest(input.array());
                byte[] payload = new byte[h.length + sig.length];
                System.arraycopy(h,0,payload,0,h.length);
                System.arraycopy(sig,0,payload,h.length,sig.length);
                server.send((SocketChannel) selectionKey.channel(),payload);
                //server.send((SocketChannel) selectionKey.channel(),MessageDigest.getInstance("MD5").digest(input.array()));
                
                
            } else {
            
                // Old handshake method with no challenge:
                rc.addHeader(WEBSOCKET_ORIGIN, requestHeaders.get(ORIGIN));
                rc.addHeader(WEBSOCKET_LOCATION, getWebSocketLocation());
                String protocol = requestHeaders.get(WEBSOCKET_PROTOCOL);
                if (protocol != null) {
                    rc.addHeader(WEBSOCKET_PROTOCOL, protocol);
                }
                sendHttpResponse(rc, false);
            }

            
            return;
        }

        // Send an error page otherwise.
        rc.setResponseStatus(FORBIDDEN);
        rc.addHeader(CONNECTION, "close");
        sendHttpResponse(rc, true);
    }

	
    private void handleWebSocketFrames(byte[] packet) throws Exception {
    	
    	byte[] rawpacket = new byte[packet.length-1];
    	System.arraycopy(packet,1,rawpacket,0,rawpacket.length);
    	
    	String uppercased = new String(rawpacket,"UTF-8");
    	uppercased = uppercased.toUpperCase();
    	
    	
    	ByteBuffer buffer = ByteBuffer.allocate(uppercased.length()+2);
    	buffer.put(WS_PACKET_START_BYTE);
    	buffer.put(uppercased.getBytes("UTF-8"));
    	buffer.put(WS_PACKET_END_BYTE);
    	
        server.send((SocketChannel) selectionKey.channel(), buffer.array());
    }
    
	
    private void sendHttpResponse(HTTPResponseCreator rc, boolean closeConnection) {
    	SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
    	String message = rc.getMessage();
    	System.out.println(message);//!@#$@#$#@$#@$#@#@#@
        server.send(socketChannel, message.getBytes());
    	
    	/*if (closeConnection){
    		try {
    			server.removeWorker(socketChannel);
    			socketChannel.close();
    		} catch(Exception e){}
    	}*/
    }
	
    private String getWebSocketLocation() {
        return "ws://" + (String)requestHeaders.get(HOST) + webSocketPath;
    }
	
	public Object clone() {
		try {
			WebSocketWorkerTask cloned = (WebSocketWorkerTask) this.getClass().newInstance();
			cloned.setWebSocketPath(this.webSocketPath);
			cloned.state = START_STATE;
			return cloned;
		} catch (Exception e) {
			return null;
		}
	}
}
